Category 底层原理研究

Category 的加载处理过程

在这篇博客 iOS 程序 main 函数之前发生了什么 有中提到,_objc_init 这个函数是 runtime 系统的初始化函数,于是我们可以直接从 _objc_init 这个函数开始进行分析, Category 加载过程中的函数调用顺序如下:

1
2
3
4
5
6
7
void _objc_init(void);
└── void map_images(...);
└── void map_images_nolock(...);
└── void _read_images(...);
└── static void addUnattachedCategoryForClass(...);
└── static void remethodizeClass(Class cls);
└── static void attachCategories(Class cls, category_list *cats, bool flush_caches);
文件名 方法
objc-os.mm _objc_init
objc-os.mm map_images
objc-os.mm map_images_nolock
objc-runtime-new.mm _read_images
objc-runtime-new.mm addUnattachedCategoryForClass
objc-runtime-new.mm remethodizeClass
objc-runtime-new.mm attachCategories
objc-runtime-new.mm attachLists

_read_images 函数处理当前镜像文件的头部信息,具体步骤:

  1. 获取镜像文件中的类列表,遍历列表进行类读取(调用readClass
  2. 遍历注册所有的selector名字(调用__sel_registerName
  3. 遍历读取协议列表(调用_getObjc2ProtocolList, readProtocol
  4. 遍历读取分类列表(调用_getObjc2CategoryList, addUnattachedCategoryForClass, remethodizeClass
  5. 遍历实例化运行时类结构(调用realizeClass

从中可以找到与分类相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);

if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}

if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}

在上面的代码中,首先通过函数 _getObjc2CategoryList 获取 category 的列表 catlist,然后遍历 catlist,获取 category 的 Class,根据 Class(类对象和元类对象) 的实例方法、协议、属性,来判断调用addUnattachedCategoryForClass 函数,并进一步判断是否调用 remethodizeClass 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
header_info *catHeader)
{
runtimeLock.assertWriting();

// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;

list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
static NXMapTable *unattachedCategories(void)
{
runtimeLock.assertWriting();

static NXMapTable *category_map = nil;
if (category_map) return category_map;
// fixme initial map size
category_map = NXCreateMapTable(NXPtrValueMapPrototype, 16);
return category_map;
}

addUnattachedCategoryForClass 函数中通过 unattachedCategories() 函数生成一个单例 MapTable 对象 cats,从该对象中获取 category 的 list 指针,判断该 list 指针是否为空,分配相应内存空间,最后将 category 的数据插入到单例 MapTable 中。

remethodizeClass 函数中将通过 attachCategories 函数,把分类信息附加到相应的类中。attachCategories 函数会将类别中的方法列表,属性和协议列表分别都加入本类中,并假定了类别列表加载的顺序是根据类别文件的加载顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

// 重新分配内存
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));

// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}

protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}

auto rw = cls->data();

prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);

rw->properties.attachLists(proplists, propcount);
free(proplists);

rw->protocols.attachLists(protolists, protocount);
free(protolists);
}

其中的加载函数为 attachLists,其关键实现为:

1
2
3
4
5
array()->count = newCount;
memmove(array()->lists + addedCount,
array()->lists, oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists,
addedLists, addedCount * sizeof(array()->lists[0]));

attachLists 方法主要关注两个变量 array()->listsaddedLists

  • array()->lists: 类对象原来的方法列表,属性列表,协议列表
  • addedLists:传入所有分类的方法列表,属性列表,协议列表

上面代码的作用就是通过 memmove 将原来的类找那个的方法、属性、协议列表分别进行后移,然后通过 memcpy 将传入的方法、属性、协议列表填充到开始的位置。

这里总结一下上面的过程:

1、通过 Runtime 加载某个类的所有 Category 数据

2、把所有 Category 的方法、属性、协议数据,合并到一个大数组中,后面参与编译的 Category 数据,会在数组的前面

3、将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

拓展

load 源码分析

通过 objc4 中的源码进行分析, load 加载过程中的函数调用顺序如下:

1
2
3
4
void _objc_init(void);
└── void load_images(...);
└── void call_load_methods(...);
└── void call_class_loads(...);

load_images 函数中核心逻辑是调用 prepare_load_methodscall_load_methods

prepare_load_methods 函数的作用就是提前准备好满足 +load 方法调用条件的类和分类,以供接下来的调用。 然后在这个类中调用了schedule_class_load(Class cls)方法,并且在入参时对父类递归的调用了,确保父类优先的顺序。

call_load_methods 函数中循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load);+load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。

  • 分析 call_load_methods 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void call_load_methods(void)
{
static BOOL loading = NO;
BOOL more_categories;

recursive_mutex_assert_locked(&loadMethodLock);

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

从中可以看出

1、通过 do-while 循环加载类的 load 方法(call_class_loads 是实现 +load 方法的核心函数);

2、先调用完所有类的 load 方法,再调用分类的 load 方法。

  • 分析 call_class_loads 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void call_class_loads(void)
{
int i;

// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 找到类中的 load 方法,并初始化一个指针指向它
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 通过 load 方法的内存地址直接调用
(*load_method)(cls, SEL_load);
}

// Destroy the detached list.
if (classes) _free_internal(classes);
}

这个函数的作用就是真正负责调用类的 +load 方法了。它从全局变量 loadable_classes 中取出所有可供调用的类,并进行清零操作,其中 loadable_classes 指向用于保存类信息的内存的首地址,loadable_classes_allocated标识已分配的内存空间大小,loadable_classes_used 则标识已使用的内存空间大小。然后,循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。

+ (void)load 小总结

  1. +load 方法会在 runtime 加载类、分类时调用
  2. 每个类、分类的+load,在程序运行过程中只调用一次
  3. 调用顺序:父类 -> 子类 -> 父类的 category -> 子类的 category
1
2
3
4
2018-12-03 15:33:36.915480+0800 CALayerDemo[20979:460542] Foo +[Foo load]
2018-12-03 15:33:36.916050+0800 CALayerDemo[20979:460542] SubFoo +[SubFoo load]
2018-12-03 15:33:36.916125+0800 CALayerDemo[20979:460542] Foo(Test) +[Foo(Test) load]
2018-12-03 15:33:36.916196+0800 CALayerDemo[20979:460542] SubFoo(Test) +[SubFoo(Test) load]

initialize 源码分析

通过 objc4 中的源码进行分析, initialize 加载过程中的函数调用顺序如下:

1
2
3
4
5
Method class_getInstanceMethod(Class cls, SEL sel);
└── IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);
└── IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);
└── void _class_initialize(Class cls);
└── void callInitialize(Class cls);

+ (void)initialize 小总结

  1. +initialize 方法会在类第一次接收到消息时调用
  2. 先调用父类的 +initialize,再调用子类的 +initialize
  3. 先初始化父类,再初始化子类,每个类只会初始化1次
1
2
2018-12-03 15:33:36.916315+0800 CALayerDemo[20979:460542] Foo(Test) +[Foo(Test) initialize]
2018-12-03 15:33:36.916390+0800 CALayerDemo[20979:460542] SubFoo(Test) +[SubFoo(Test) initialize]

load 与 initialize对比

条件 +load +initialize
关键方法 (*load_method)(cls, SEL_load) objc_msgSend
调用时机 被添加到 runtime 时 收到第一条消息前,可能永远不调用
调用顺序 父类 -> 子类 -> 父类分类 -> 子类分类 父类 -> 子类或(父类分类 -> 子类分类)
调用次数 1次 多次
是否需要显式调用父类实现
是否沿用父类的实现
分类中的实现 类和分类都执行 覆盖类中的方法,只执行分类的实现

参考

iOS 程序 main 函数之前发生了什么

Objc runtime 初始化过程分析